package org.spiderflow.core.expression.parsing.ast.call;

import org.apache.commons.lang3.StringUtils;
import org.spiderflow.core.expression.ExpressionError;
import org.spiderflow.core.expression.ExpressionTemplate;
import org.spiderflow.core.expression.ExpressionTemplateContext;
import org.spiderflow.core.expression.parsing.Span;
import org.spiderflow.core.expression.parsing.ast.Expression;
import org.spiderflow.core.expression.parsing.ast.access.MemberAccess;
import org.spiderflow.core.expression.reflection.JavaReflection;
import org.spiderflow.common.expression.DynamicMethod;

import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

/**
 * Represents a call to a method of the form <code>object.method(a, b, c)</code>.
 *
 * @author zsh
 */
public class MethodCall extends BaseCall {
    private final MemberAccess accessMethod;
    private Method cachedMethod;

    public MethodCall(Span span, MemberAccess accessMethod, List<Expression> arguments) {
        super(span, arguments);
        this.accessMethod = accessMethod;
    }

    /**
     * Returns the object on which to call the method.
     **/
    private Expression getCallMethodObject() {
        return accessMethod.getObject();
    }

    /**
     * Returns the method to call.
     **/
    private MemberAccess getAccessMethod() {
        return accessMethod;
    }

    /**
     * Returns the cached member descriptor as returned by {@link JavaReflection#getMethod(Object, String, Object...)}.
     * See {@link #setCachedMethod(Method)}.
     **/
    private Method getCachedMethod() {
        return cachedMethod;
    }

    /**
     * Sets the method descriptor as returned by {@link JavaReflection#getMethod(Object, String, Object...)} for faster lookups.
     * Called by {@link org.spiderflow.core.expression.interpreter.AstInterpreter} the first time this node is evaluated. Subsequent evaluations can use the cached
     * descriptor, avoiding a costly reflective lookup.
     **/
    private void setCachedMethod(Method cachedMethod) {
        this.cachedMethod = cachedMethod;
    }

    @Override
    public Object evaluate(ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
        try {
            Object callMethodObject = getCallMethodObject().evaluate(template, context);
            if (callMethodObject == null) {
                return null;
            }
            Object[] argumentValues = getArgumentValues(template, context);
            Object res = tryDirectExecuteMethod(callMethodObject, argumentValues);
            if (isSuccess(res)) {
                return res;
            }
            // Otherwise, try to find a corresponding method or field pointing to a lambda.
            res = tryExecuteMethodByCache(callMethodObject, argumentValues);
            if (isSuccess(res)) {
                return res;
            }
            //try to find the method on the object
            res = tryExecuteMethodByReflection(callMethodObject, argumentValues);
            if (isSuccess(res)) {
                return res;
            }
            //try to find the method on the extension of object
            res = tryExecuteMethodByExtension(callMethodObject, argumentValues);
            if (isSuccess(res)) {
                return res;
            }
            // didn't find the method on the object, try to find a field pointing to a lambda
            return finalTryExecuteMethod(callMethodObject, argumentValues);
        } finally {
            clearCachedArguments();
        }
    }

    private Object tryDirectExecuteMethod(Object callMethodObject, Object[] argumentValues) {
        if (!(callMethodObject instanceof DynamicMethod)) {
            return FAIL_FLAG;
        }
        try {
            Object method = DynamicMethod.class.getDeclaredMethod("execute", String.class, List.class);
            Object[] newArgumentValues = new Object[]{getAccessMethod().getName().getText(), Arrays.asList(argumentValues)};
            return JavaReflection.getInstance().callMethod(callMethodObject, method, newArgumentValues);
        } catch (RuntimeException | NoSuchMethodException t) {
            throw ExpressionError.error(t.getMessage(), getSpan(), t);
        }
    }

    private Object tryExecuteMethodByCache(Object callMethodObject, Object[] argumentValues) {
        Method method = getCachedMethod();
        if (method == null) {
            return FAIL_FLAG;
        }
        try {
            return JavaReflection.getInstance().callMethod(callMethodObject, method, argumentValues);
        } catch (RuntimeException t) {
            // fall through
            return FAIL_FLAG;
        }
    }

    private Object tryExecuteMethodByReflection(Object callMethodObject, Object[] argumentValues) {
        Method method = JavaReflection.getInstance().getMethod(callMethodObject, getAccessMethod().getName().getText(), argumentValues);
        if (method == null) {
            return FAIL_FLAG;
        }
        // found the method on the object, call it
        setCachedMethod(method);
        try {
            return JavaReflection.getInstance().callMethod(callMethodObject, method, argumentValues);
        } catch (RuntimeException t) {
            throw ExpressionError.error(t.getMessage(), getSpan(), t);
        }
    }

    private Object tryExecuteMethodByExtension(Object callMethodObject, Object[] argumentValues) {
        Method method = JavaReflection.getInstance().getExtensionMethod(callMethodObject, getAccessMethod().getName().getText(), argumentValues);
        if (method == null) {
            return FAIL_FLAG;
        }
        try {
            int argumentLength = argumentValues.length;
            Object[] parameters = new Object[argumentLength + 1];
            System.arraycopy(argumentValues, 0, parameters, 1, argumentLength);
            parameters[0] = callMethodObject;
            if (callMethodObject.getClass().isArray()) {
                Object[] objs = new Object[Array.getLength(callMethodObject)];
                for (int i = 0, len = objs.length; i < len; i++) {
                    Array.set(objs, i, Array.get(callMethodObject, i));
                }
                parameters[0] = objs;
            }
            return JavaReflection.getInstance().callMethod(callMethodObject, method, parameters);
        } catch (RuntimeException t) {
            throw ExpressionError.error(t.getMessage(), getSpan(), t);
        }
    }

    private Object finalTryExecuteMethod(Object callMethodObject, Object[] argumentValues) {
        Field field = JavaReflection.getInstance().getField(callMethodObject, getAccessMethod().getName().getText());
        if (field == null) {
            throw ExpressionError.error("在'" + callMethodObject.getClass() + "'中找不到方法 " + getAccessMethod().getName().getText() + "(" + StringUtils.join(JavaReflection.getStringTypes(argumentValues), ",") + ")",
                    getSpan());
        }
        Object function = JavaReflection.getInstance().getFieldValue(callMethodObject, field);
        Method method = JavaReflection.getInstance().getMethod(function, null, argumentValues);
        if (method == null) {
            throw ExpressionError.error("在'" + callMethodObject.getClass() + "'中找不到方法 " + getAccessMethod().getName().getText() + "(" + StringUtils.join(JavaReflection.getStringTypes(argumentValues), ",") + ")",
                    getSpan());
        }
        try {
            return JavaReflection.getInstance().callMethod(function, method, argumentValues);
        } catch (RuntimeException t) {
            throw ExpressionError.error(t.getMessage(), getSpan(), t);
        }
    }
}